JavaScript'te gerçek çoklu iş parçacığının kilidini açın. Bu kapsamlı kılavuz, SharedArrayBuffer, Atomics, Web Workers ve yüksek performanslı web uygulamaları için güvenlik gereksinimlerini ele almaktadır.
JavaScript SharedArrayBuffer: Web'de Eşzamanlı Programlamaya Derinlemesine Bir Bakış
Onlarca yıldır JavaScript'in tek iş parçacıklı (single-threaded) doğası, hem basitliğinin bir kaynağı hem de önemli bir performans darboğazı olmuştur. Olay döngüsü (event loop) modeli, çoğu arayüz odaklı görev için harika çalışır, ancak hesaplama açısından yoğun işlemlerle karşılaştığında zorlanır. Uzun süren hesaplamalar tarayıcıyı dondurarak sinir bozucu bir kullanıcı deneyimi yaratabilir. Web Workers, betiklerin arka planda çalışmasına izin vererek kısmi bir çözüm sunsa da, kendi büyük sınırlamalarıyla birlikte geldiler: verimsiz veri iletişimi.
İşte bu noktada SharedArrayBuffer
(SAB), web'de iş parçacıkları arasında gerçek, düşük seviyeli bellek paylaşımını sunarak oyunu temelden değiştiren güçlü bir özellik olan devreye giriyor. Atomics
nesnesiyle eşleştirilen SAB, doğrudan tarayıcıda yeni bir yüksek performanslı, eşzamanlı uygulamalar çağının kilidini açar. Ancak, büyük güç büyük sorumluluk ve karmaşıklık getirir.
Bu kılavuz sizi JavaScript'te eşzamanlı programlama dünyasına derinlemesine bir yolculuğa çıkaracak. Neden buna ihtiyacımız olduğunu, SharedArrayBuffer
ve Atomics
'in nasıl çalıştığını, ele almanız gereken kritik güvenlik hususlarını ve başlamanıza yardımcı olacak pratik örnekleri inceleyeceğiz.
Eski Dünya: JavaScript'in Tek İş Parçacıklı Modeli ve Sınırlamaları
Çözümü takdir edebilmek için önce sorunu tam olarak anlamalıyız. Bir tarayıcıdaki JavaScript yürütmesi, geleneksel olarak "ana iş parçacığı" veya "UI iş parçacığı" olarak adlandırılan tek bir iş parçacığında gerçekleşir.
Olay Döngüsü (Event Loop)
Ana iş parçacığı her şeyden sorumludur: JavaScript kodunuzu yürütmek, sayfayı oluşturmak, kullanıcı etkileşimlerine (tıklamalar ve kaydırmalar gibi) yanıt vermek ve CSS animasyonlarını çalıştırmak. Bu görevleri, sürekli olarak bir mesaj (görev) kuyruğunu işleyen bir olay döngüsü kullanarak yönetir. Bir görevin tamamlanması uzun sürerse, tüm kuyruğu engeller. Başka hiçbir şey olamaz—kullanıcı arayüzü donar, animasyonlar takılır ve sayfa yanıt vermez hale gelir.
Web Workers: Doğru Yönde Bir Adım
Web Workers bu sorunu azaltmak için tanıtıldı. Bir Web Worker, esasen ayrı bir arka plan iş parçacığında çalışan bir betiktir. Ağır hesaplamaları bir worker'a yükleyerek ana iş parçacığını kullanıcı arayüzünü yönetmek için serbest bırakabilirsiniz.
Ana iş parçacığı ile bir worker arasındaki iletişim postMessage()
API'si aracılığıyla gerçekleşir. Veri gönderdiğinizde, bu veri yapılandırılmış klon algoritması tarafından işlenir. Bu, verinin serileştirildiği, kopyalandığı ve ardından worker'ın bağlamında seriden çıkarıldığı anlamına gelir. Etkili olsa da, bu sürecin büyük veri setleri için önemli dezavantajları vardır:
- Performans Yükü: İş parçacıkları arasında megabaytlarca hatta gigabaytlarca veri kopyalamak yavaş ve CPU yoğundur.
- Bellek Tüketimi: Bellekte verinin bir kopyasını oluşturur, bu da bellek kısıtlı cihazlar için büyük bir sorun olabilir.
Tarayıcıda bir video düzenleyici hayal edin. Saniyede 60 kez işlemek için bir video karesinin tamamını (birkaç megabayt olabilir) bir worker'a ileri geri göndermek, fahiş derecede pahalı olurdu. İşte SharedArrayBuffer
tam olarak bu sorunu çözmek için tasarlandı.
Oyunun Kurallarını Değiştiren: SharedArrayBuffer
ile Tanışın
Bir SharedArrayBuffer
, bir ArrayBuffer
'a benzer şekilde, sabit uzunlukta bir ham ikili veri arabelleğidir. Kritik fark, bir SharedArrayBuffer
'ın birden fazla iş parçacığı (örneğin, ana iş parçacığı ve bir veya daha fazla Web Worker) arasında paylaşılabilmesidir. postMessage()
kullanarak bir SharedArrayBuffer
"gönderdiğinizde", bir kopya göndermiyorsunuz; aynı bellek bloğuna bir referans gönderiyorsunuz.
Bu, bir iş parçacığı tarafından arabellek verilerinde yapılan herhangi bir değişikliğin, ona referansı olan diğer tüm iş parçacıkları tarafından anında görülebildiği anlamına gelir. Bu, maliyetli kopyala-serileştir adımını ortadan kaldırarak neredeyse anlık veri paylaşımını mümkün kılar.
Şöyle düşünün:
postMessage()
ile Web Workers: Bu, iki iş arkadaşının bir belge üzerinde e-posta ile kopyalarını birbirlerine göndererek çalışmasına benzer. Her değişiklik, yepyeni bir kopyanın gönderilmesini gerektirir.SharedArrayBuffer
ile Web Workers: Bu, iki iş arkadaşının aynı belge üzerinde paylaşılan bir çevrimiçi düzenleyicide (Google Docs gibi) çalışmasına benzer. Değişiklikler her ikisi tarafından da gerçek zamanlı olarak görülebilir.
Paylaşılan Belleğin Tehlikesi: Yarış Durumları (Race Conditions)
Anlık bellek paylaşımı güçlüdür, ancak aynı zamanda eşzamanlı programlama dünyasından klasik bir sorunu da beraberinde getirir: yarış durumları.
Bir yarış durumu, birden fazla iş parçacığı aynı paylaşılan veriye aynı anda erişmeye ve değiştirmeye çalıştığında ve nihai sonucun, hangi sırayla çalıştıklarının öngörülemeyen düzenine bağlı olduğunda ortaya çıkar. Bir SharedArrayBuffer
'da saklanan basit bir sayacı düşünün. Hem ana iş parçacığı hem de bir worker onu artırmak istiyor.
- A İş Parçacığı mevcut değeri okur, değer 5'tir.
- A İş Parçacığı yeni değeri yazamadan, işletim sistemi onu duraklatır ve B İş Parçacığı'na geçer.
- B İş Parçacığı mevcut değeri okur, değer hala 5'tir.
- B İş Parçacığı yeni değeri (6) hesaplar ve belleğe geri yazar.
- Sistem tekrar A İş Parçacığı'na döner. B İş Parçacığı'nın bir şey yaptığını bilmez. Kaldığı yerden devam eder, yeni değerini (5 + 1 = 6) hesaplar ve 6'yı belleğe geri yazar.
Sayaç iki kez artırılmasına rağmen, nihai değer 7 değil, 6'dır. İşlemler atomik değildi—kesintiye uğrayabilirlerdi ve bu da veri kaybına yol açtı. İşte tam da bu yüzden bir SharedArrayBuffer
'ı onun hayati ortağı olan Atomics
nesnesi olmadan kullanamazsınız.
Paylaşılan Belleğin Koruyucusu: Atomics
Nesnesi
Atomics
nesnesi, SharedArrayBuffer
nesneleri üzerinde atomik işlemler gerçekleştirmek için bir dizi statik metot sağlar. Bir atomik işlemin, başka herhangi bir işlem tarafından kesintiye uğratılmadan tamamen gerçekleştirileceği garanti edilir. Ya tamamen gerçekleşir ya da hiç gerçekleşmez.
Atomics
kullanmak, paylaşılan bellek üzerindeki oku-değiştir-yaz işlemlerinin güvenli bir şekilde yapılmasını sağlayarak yarış durumlarını önler.
Temel Atomics
Metotları
Atomics
tarafından sağlanan en önemli metotlardan bazılarına bakalım.
Atomics.load(typedArray, index)
: Atomik olarak belirli bir dizindeki değeri okur ve döndürür. Bu, tam ve bozulmamış bir değer okuduğunuzdan emin olmanızı sağlar.Atomics.store(typedArray, index, value)
: Atomik olarak bir değeri belirli bir dizine depolar ve o değeri döndürür. Bu, yazma işleminin kesintiye uğramamasını sağlar.Atomics.add(typedArray, index, value)
: Atomik olarak belirli bir dizindeki değere bir değer ekler. O konumdaki orijinal değeri döndürür. Bu,x += value
işleminin atomik eşdeğeridir.Atomics.sub(typedArray, index, value)
: Atomik olarak belirli bir dizindeki değerden bir değer çıkarır.Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
: Bu güçlü bir koşullu yazma işlemidir.index
'teki değerinexpectedValue
'ya eşit olup olmadığını kontrol eder. Eğer eşitse, onureplacementValue
ile değiştirir ve orijinalexpectedValue
'yu döndürür. Değilse, hiçbir şey yapmaz ve mevcut değeri döndürür. Bu, kilitler gibi daha karmaşık senkronizasyon mekanizmalarını uygulamak için temel bir yapı taşıdır.
Senkronizasyon: Basit İşlemlerin Ötesi
Bazen güvenli okuma ve yazmadan daha fazlasına ihtiyacınız olur. İş parçacıklarının koordine olması ve birbirini beklemesi gerekir. Yaygın bir anti-desen, bir iş parçacığının sıkı bir döngüde oturup bir bellek konumunda değişiklik olup olmadığını sürekli kontrol ettiği "meşgul bekleme"dir. Bu, CPU döngülerini boşa harcar ve pil ömrünü tüketir.
Atomics
, wait()
ve notify()
ile çok daha verimli bir çözüm sunar.
Atomics.wait(typedArray, index, value, timeout)
: Bu, bir iş parçacığına uyku moduna geçmesini söyler.index
'teki değerin halavalue
olup olmadığını kontrol eder. Eğer öyleyse, iş parçacığıAtomics.notify()
tarafından uyandırılana veya isteğe bağlıtimeout
(milisaniye cinsinden) süresine ulaşılana kadar uyur. Eğerindex
'teki değer zaten değişmişse, hemen geri döner. Uyuyan bir iş parçacığı neredeyse hiç CPU kaynağı tüketmediği için bu inanılmaz derecede verimlidir.Atomics.notify(typedArray, index, count)
: Bu,Atomics.wait()
aracılığıyla belirli bir bellek konumunda uyuyan iş parçacıklarını uyandırmak için kullanılır. En fazlacount
kadar bekleyen iş parçacığını (veyacount
sağlanmazsa ya daInfinity
ise hepsini) uyandırır.
Hepsini Bir Araya Getirmek: Pratik Bir Kılavuz
Şimdi teoriyi anladığımıza göre, SharedArrayBuffer
kullanarak bir çözüm uygulama adımlarını gözden geçirelim.
Adım 1: Güvenlik Ön Koşulu - Cross-Origin İzolasyonu
Bu, geliştiriciler için en yaygın engeldir. Güvenlik nedenleriyle, SharedArrayBuffer
yalnızca cross-origin isolated (çapraz köken yalıtılmış) durumdaki sayfalarda kullanılabilir. Bu, Spectre gibi spekülatif yürütme güvenlik açıklarını azaltmak için bir güvenlik önlemidir. Bu açıklar, potansiyel olarak (paylaşılan bellek sayesinde mümkün olan) yüksek çözünürlüklü zamanlayıcıları kullanarak kökenler arasında veri sızdırabilir.
Cross-origin izolasyonunu etkinleştirmek için, web sunucunuzu ana belgeniz için iki özel HTTP başlığı gönderecek şekilde yapılandırmanız gerekir:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
(COOP): Belgenizin tarama bağlamını diğer belgelerden yalıtarak, onların pencere nesnenizle doğrudan etkileşim kurmasını engeller.Cross-Origin-Embedder-Policy: require-corp
(COEP): Sayfanız tarafından yüklenen tüm alt kaynakların (resimler, betikler ve iframe'ler gibi) ya aynı kökenden olmasını ya daCross-Origin-Resource-Policy
başlığı veya CORS ile açıkça çapraz köken yüklenebilir olarak işaretlenmesini gerektirir.
Bunu kurmak zor olabilir, özellikle de gerekli başlıkları sağlamayan üçüncü taraf betiklerine veya kaynaklarına güveniyorsanız. Sunucunuzu yapılandırdıktan sonra, tarayıcının konsolunda self.crossOriginIsolated
özelliğini kontrol ederek sayfanızın yalıtılmış olup olmadığını doğrulayabilirsiniz. Bu değer true
olmalıdır.
Adım 2: Arabelleği Oluşturma ve Paylaşma
Ana betiğinizde, SharedArrayBuffer
'ı ve üzerinde Int32Array
gibi bir TypedArray
kullanarak bir "görünüm" oluşturursunuz.
main.js:
// Önce cross-origin izolasyonunu kontrol et!
if (!self.crossOriginIsolated) {
console.error("Bu sayfa cross-origin isolated değil. SharedArrayBuffer kullanılamayacak.");
} else {
// Bir adet 32-bit tamsayı için paylaşılan bir arabellek oluştur.
const buffer = new SharedArrayBuffer(4);
// Arabellek üzerinde bir görünüm oluştur. Tüm atomik işlemler görünüm üzerinde gerçekleşir.
const int32Array = new Int32Array(buffer);
// 0 indeksindeki değeri başlat.
int32Array[0] = 0;
// Yeni bir worker oluştur.
const worker = new Worker('worker.js');
// PAYLAŞILAN arabelleği worker'a gönder. Bu bir kopya değil, referans transferidir.
worker.postMessage({ buffer });
// Worker'dan gelen mesajları dinle.
worker.onmessage = (event) => {
console.log(`Worker tamamlandığını bildirdi. Nihai değer: ${Atomics.load(int32Array, 0)}`);
};
}
Adım 3: Worker'da Atomik İşlemler Gerçekleştirme
Worker arabelleği alır ve artık üzerinde atomik işlemler gerçekleştirebilir.
worker.js:
self.onmessage = (event) => {
const { buffer } = event.data;
const int32Array = new Int32Array(buffer);
console.log("Worker paylaşılan arabelleği aldı.");
// Birkaç atomik işlem yapalım.
for (let i = 0; i < 1000000; i++) {
// Paylaşılan değeri güvenli bir şekilde artır.
Atomics.add(int32Array, 0, 1);
}
console.log("Worker artırma işlemini bitirdi.");
// Bittiğimizi ana iş parçacığına bildir.
self.postMessage({ done: true });
};
Adım 4: Daha Gelişmiş Bir Örnek - Senkronizasyon ile Paralel Toplama
Daha gerçekçi bir sorunu ele alalım: çok büyük bir sayı dizisini birden çok worker kullanarak toplamak. Verimli senkronizasyon için Atomics.wait()
ve Atomics.notify()
kullanacağız.
Paylaşılan arabelleğimizin üç bölümü olacak:
- İndeks 0: Bir durum bayrağı (0 = işleniyor, 1 = tamamlandı).
- İndeks 1: Kaç worker'ın bitirdiğini sayan bir sayaç.
- İndeks 2: Nihai toplam.
main.js:
if (self.crossOriginIsolated) {
const NUM_WORKERS = 4;
const DATA_SIZE = 10_000_000;
// [durum, biten_workerlar, sonuc_dusuk, sonuc_yuksek]
// Büyük toplamlar için taşmayı önlemek amacıyla sonuç için iki 32-bit tamsayı kullanıyoruz.
const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 tamsayı
const sharedArray = new Int32Array(sharedBuffer);
// İşlemek için rastgele veri oluştur
const data = new Uint8Array(DATA_SIZE);
for (let i = 0; i < DATA_SIZE; i++) {
data[i] = Math.floor(Math.random() * 10);
}
const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);
for (let i = 0; i < NUM_WORKERS; i++) {
const worker = new Worker('sum_worker.js');
const start = i * chunkSize;
const end = Math.min(start + chunkSize, DATA_SIZE);
// Worker'ın veri parçası için paylaşılmayan bir görünüm oluştur
const dataChunk = data.subarray(start, end);
worker.postMessage({
sharedBuffer,
dataChunk // Bu kopyalanır
});
}
console.log('Ana iş parçacığı şimdi worker\'ların bitirmesini bekliyor...');
// 0 indeksindeki durum bayrağının 1 olmasını bekle
// Bu, bir while döngüsünden çok daha iyidir!
Atomics.wait(sharedArray, 0, 0); // sharedArray[0] 0 ise bekle
console.log('Ana iş parçacığı uyandırıldı!');
const finalSum = Atomics.load(sharedArray, 2);
console.log(`Nihai paralel toplam: ${finalSum}`);
} else {
console.error('Sayfa cross-origin isolated değil.');
}
sum_worker.js:
self.onmessage = ({ data }) => {
const { sharedBuffer, dataChunk } = data;
const sharedArray = new Int32Array(sharedBuffer);
// Bu worker'ın parçası için toplamı hesapla
let localSum = 0;
for (let i = 0; i < dataChunk.length; i++) {
localSum += dataChunk[i];
}
// Yerel toplamı atomik olarak paylaşılan toplama ekle
Atomics.add(sharedArray, 2, localSum);
// 'biten workerlar' sayacını atomik olarak artır
const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;
// Eğer bu bitiren son worker ise...
const NUM_WORKERS = 4; // Gerçek bir uygulamada bu değerin parametre olarak geçilmesi gerekir
if (finishedCount === NUM_WORKERS) {
console.log('Son worker bitirdi. Ana iş parçacığına bildiriliyor.');
// 1. Durum bayrağını 1'e ayarla (tamamlandı)
Atomics.store(sharedArray, 0, 1);
// 2. 0 indeksinde bekleyen ana iş parçacığını uyar
Atomics.notify(sharedArray, 0, 1);
}
};
Gerçek Dünya Kullanım Alanları ve Uygulamaları
Bu güçlü ama karmaşık teknoloji gerçekte nerede bir fark yaratıyor? Büyük veri setleri üzerinde ağır, paralelleştirilebilir hesaplama gerektiren uygulamalarda üstün başarı gösterir.
- WebAssembly (Wasm): Bu, en can alıcı kullanım alanıdır. C++, Rust ve Go gibi dillerin çoklu iş parçacığı için olgun bir desteği vardır. Wasm, geliştiricilerin bu mevcut yüksek performanslı, çok iş parçacıklı uygulamaları (oyun motorları, CAD yazılımları ve bilimsel modeller gibi) tarayıcıda çalışacak şekilde derlemelerine olanak tanır ve iş parçacığı iletişimi için temel mekanizma olarak
SharedArrayBuffer
kullanır. - Tarayıcı İçi Veri İşleme: Büyük ölçekli veri görselleştirme, istemci tarafı makine öğrenmesi model çıkarımı ve büyük miktarda veri işleyen bilimsel simülasyonlar önemli ölçüde hızlandırılabilir.
- Medya Düzenleme: Yüksek çözünürlüklü görüntülere filtre uygulamak veya bir ses dosyası üzerinde ses işleme yapmak, parçalara ayrılıp birden fazla worker tarafından paralel olarak işlenebilir ve kullanıcıya gerçek zamanlı geri bildirim sağlanabilir.
- Yüksek Performanslı Oyun: Modern oyun motorları fizik, yapay zeka ve varlık yüklemesi için büyük ölçüde çoklu iş parçacığına dayanır.
SharedArrayBuffer
, tamamen tarayıcıda çalışan konsol kalitesinde oyunlar oluşturmayı mümkün kılar.
Zorluklar ve Son Değerlendirmeler
SharedArrayBuffer
dönüştürücü bir teknoloji olsa da, her derde deva değildir. Dikkatli kullanım gerektiren düşük seviyeli bir araçtır.
- Karmaşıklık: Eşzamanlı programlama herkesin bildiği gibi zordur. Yarış durumlarını ve kilitlenmeleri (deadlocks) ayıklamak inanılmaz derecede zor olabilir. Uygulama durumunuzun nasıl yönetildiği hakkında farklı düşünmeniz gerekir.
- Kilitlenmeler (Deadlocks): Bir kilitlenme, iki veya daha fazla iş parçacığı sonsuza dek engellendiğinde, her biri diğerinin bir kaynağı serbest bırakmasını beklediğinde meydana gelir. Karmaşık kilitleme mekanizmalarını yanlış uygularsanız bu durum ortaya çıkabilir.
- Güvenlik Yükü: Cross-origin izolasyon gereksinimi önemli bir engeldir. Gerekli CORS/CORP başlıklarını desteklemiyorlarsa üçüncü taraf hizmetleri, reklamlar ve ödeme ağ geçitleri ile entegrasyonları bozabilir.
- Her Sorun İçin Değil: Basit arka plan görevleri veya G/Ç işlemleri için,
postMessage()
ile geleneksel Web Worker modeli genellikle daha basit ve yeterlidir. Sadece büyük miktarda veri içeren, açıkça CPU'ya bağlı bir darboğazınız olduğundaSharedArrayBuffer
'a başvurun.
Sonuç
SharedArrayBuffer
, Atomics
ve Web Workers ile birlikte, web geliştirme için bir paradigma kaymasını temsil eder. Tek iş parçacıklı modelin sınırlarını yıkarak, yeni bir sınıf güçlü, performanslı ve karmaşık uygulamaları tarayıcıya davet eder. Web platformunu, hesaplama açısından yoğun görevler için yerel uygulama geliştirmeyle daha eşit bir zemine yerleştirir.
Eşzamanlı JavaScript'e yolculuk, durum yönetimi, senkronizasyon ve güvenlik konularında titiz bir yaklaşım gerektiren zorlu bir süreçtir. Ancak web'de mümkün olanın sınırlarını zorlamak isteyen geliştiriciler için -gerçek zamanlı ses sentezinden karmaşık 3D renderlamaya ve bilimsel hesaplamaya kadar- SharedArrayBuffer
'da ustalaşmak artık sadece bir seçenek değil; yeni nesil web uygulamalarını oluşturmak için temel bir beceridir.